Skip to content

Add transaction support#251

Merged
doron050 merged 16 commits into
mainfrom
add-transaction-support
Jul 5, 2025
Merged

Add transaction support#251
doron050 merged 16 commits into
mainfrom
add-transaction-support

Conversation

@doron050

@doron050 doron050 commented Jun 25, 2025

Copy link
Copy Markdown
Collaborator

Support transactions in the databases

  • Add docs
  • Add tests

@doron050 doron050 linked an issue Jun 25, 2025 that may be closed by this pull request
@doron050
doron050 marked this pull request as ready for review June 25, 2025 18:10
@doron050
doron050 requested a review from SockworkOrange June 25, 2025 18:13
@Kavantix

Copy link
Copy Markdown

@doron050 any reason why you decided to create a constructor with a Transaction instead of just the Connection?

Now all the generated code does with the transaction is simply getting it's connection so that seems rather redundant

@doron050

doron050 commented Jun 27, 2025

Copy link
Copy Markdown
Collaborator Author

@doron050 any reason why you decided to create a constructor with a Transaction instead of just the Connection?

Now all the generated code does with the transaction is simply getting it's connection so that seems rather redundant

Hi @Kavantix, I'm following the sqlc best practice to maintain similarity to other SQLC plugins.
First, the generated code uses the transaction connection, but it also registers itself on the transaction object for the rollback/commit.

Please take a look at https://docs.sqlc.dev/en/stable/howto/transactions.html and see how they decided to implement transaction support. The main reason for this specific implementation is for the user to have full control over the transaction object and decide, based on their logic, when to rollback or commit.

To improve on this implementation, maybe we can do something like this:

var sqlQuery = new QuerySql(connectionString);
var transaction = sqlQuery.withTransaction();

That way, the initialization of the transaction will be inside the generated code, but again, the transaction object must be accessible from outside since the generated code can't control the commit and rollback logic.

WDYT?

@Kavantix

Copy link
Copy Markdown

@doron050 thanks for the detailed response, I see what you mean now with the registering of the transaction. The underlying libraries for C# work a lot different than the one for go with creating a new connection every time and having magic pooling and such so passing the connection seemed logical but after looking into it further having the transaction specifically is indeed required.

In the go version you indeed are in charge of the transaction yourself and you create a version of the queries struct by calling a method on the queries struct not constructing a new one.

The C# version perhaps should have a method on an existing queries object to attach a transaction to it which would create a new instance, that way you can use an injected queries class for all your queries just like in go you would use the existing queries object and get one out that will use the transaction.

There is however a slight problem with how the C# libraries work IMO, because if you want to manage the transaction yourself you suddenly also need to create the connection yourself which currently the generated code does on the fly.
So perhaps in addition to being able to make a queries object with a transaction we should also give it the power to start a new transaction.

Thus then you would use it like this:

class MyService(UsersSql userQueries, ConfigSql configQueries)
{
    void DoSomething() {
          using var transaction = userQueries.BeginTransaction();
          UsersSql userTransaction = userQueries.withTransaction(transaction);
          ConfigSql configTransaction = configQueries.withTransaction(transaction);

          await userTransaction.SomeQuery();
          await configTransaction.SomeOtherQuery();
          transaction.Commit()
    }

}

While writing this I realise though that there is another big difference with the go version. In the go version all queries are available on the same struct while the c# version generates a class per queries file. Which as you see above makes it slightly awkward to use the transaction across those queries.
Would it perhaps make sense to generate a 'helper' class that makes it easier to both begin the transaction and also in the correct way use the other QuerySql objects with or without a transaction?

@doron050

doron050 commented Jul 1, 2025

Copy link
Copy Markdown
Collaborator Author

@doron050 thanks for the detailed response, I see what you mean now with the registering of the transaction. The underlying libraries for C# work a lot different than the one for go with creating a new connection every time and having magic pooling and such so passing the connection seemed logical but after looking into it further having the transaction specifically is indeed required.

In the go version you indeed are in charge of the transaction yourself and you create a version of the queries struct by calling a method on the queries struct not constructing a new one.

The C# version perhaps should have a method on an existing queries object to attach a transaction to it which would create a new instance, that way you can use an injected queries class for all your queries just like in go you would use the existing queries object and get one out that will use the transaction.

There is however a slight problem with how the C# libraries work IMO, because if you want to manage the transaction yourself you suddenly also need to create the connection yourself which currently the generated code does on the fly. So perhaps in addition to being able to make a queries object with a transaction we should also give it the power to start a new transaction.

Thus then you would use it like this:

class MyService(UsersSql userQueries, ConfigSql configQueries)
{
    void DoSomething() {
          using var transaction = userQueries.BeginTransaction();
          UsersSql userTransaction = userQueries.withTransaction(transaction);
          ConfigSql configTransaction = configQueries.withTransaction(transaction);

          await userTransaction.SomeQuery();
          await configTransaction.SomeOtherQuery();
          transaction.Commit()
    }

}

While writing this I realise though that there is another big difference with the go version. In the go version all queries are available on the same struct while the c# version generates a class per queries file. Which as you see above makes it slightly awkward to use the transaction across those queries. Would it perhaps make sense to generate a 'helper' class that makes it easier to both begin the transaction and also in the correct way use the other QuerySql objects with or without a transaction?

Hi @Kavantix, Thanks for the long and detailed answers. After talking to @SockworkOrange about it, we decided on the following interface

class MyService(UsersSql userQueries, string connectionString)
{
    void DoSomething() {
          using var transaction = ExternalTransactionMethodCreation(connectionString);
          UsersSql userQueries = UsersSql.WithTransaction(transaction);
          ConfigSql configQueries = ConfigSql.WithTransaction(transaction);

          await userQueries.SomeQuery();
          await configQueries.SomeOtherQuery();
          transaction.Commit()
    }
}

This was decided because a connection object is not static. If we incorporate transaction creation into the code generation, users will have no control over the creation and termination of the connection object. That's probably why the sqlc go plugin did not implement the transaction creation in their version

Basically, we changed the ctor to the static WithTransaction to align the interface with the sqlc go version.

Comment thread Drivers/Generators/ExecDeclareGen.cs
Comment thread docs/03_Usage.md Outdated
Comment thread docs/03_Usage.md Outdated
| Transactions| ✅ | ✅ | ✅ |

#### Example using a transaction
```c#

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Change to uppercase C

}
"""
},
[KnownTestType.MySqlTransaction] = new TestImpl

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you use the existing constants for the test data? Just copy from any other test

}
"""
},
[KnownTestType.PostgresTransaction] = new TestImpl

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same thing, use pre-existing data constants

var connection = new Npgsql.NpgsqlConnection(Environment.GetEnvironmentVariable(EndToEndCommon.PostgresConnectionStringEnv));
await connection.OpenAsync();
var transaction = connection.BeginTransaction();
var sqlQueryWithTx = QuerySql.WithTransaction(transaction);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you are accidentally renaming querySql to sqlQuery


if (this.Transaction?.Connection == null || this.Transaction?.Connection.State != System.Data.ConnectionState.Open)
{
throw new System.InvalidOperationException("Transaction is provided, but its connection is null.");

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this a scenario that can actually happen?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the user modifies the connection in their code mid-run, then yes

@SockworkOrange SockworkOrange left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nice job!:) added a few comments

@SockworkOrange SockworkOrange left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

awesome:)

@doron050
doron050 merged commit a1a258d into main Jul 5, 2025
6 checks passed
@github-actions
github-actions Bot deleted the add-transaction-support branch August 31, 2025 19:34
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Transaction support

3 participants